环境:Nginx 1.28.3 + 宝塔面板 + Halo 2.x + Cloudflare
把博客从 Typecho 搬到 Halo 2.x,Docker Compose 跑起来,反代配好,心想总算能歇了。
结果一接 Cloudflare,控制台开始疯狂报红。再一试跨站嵌入,直接给我拒了。两个坑连环爆,折腾了一晚上,记录一下,下次别再踩了。

第一坑:HSTS 重复输出,Cloudflare 和我各说各话
咋回事呢?
我习惯在 Nginx 里开 HSTS,强制走 HTTPS:
add_header Strict-Transport-Security "max-age=31536000" always;接入 Cloudflare 后,浏览器开始骂街——Duplicate Header。
因为 Cloudflare 边缘节点本身就会统一添加 HSTS 头,回源时我再加一次,就撞车了。一个响应头出现两次,浏览器很懵:你俩到底听谁的?
我第一反应:上 if 啊!
这还不简单?判断一下有没有 Cloudflare 的特征头,没有再加 HSTS 呗:
# 直觉写法,当场翻车
if ($http_cf_ray = "") {
add_header Strict-Transport-Security "max-age=31536000" always;
}nginx -t 直接甩脸:
[emerg] "add_header" directive is not allowed here???不让我加?
后来才知道,Nginx 的 if 出了名的邪门,江湖人称 "If is Evil"。你想在 if 里塞 add_header?门儿都没有。这玩意儿只能在 server 和 location 里裸奔,不能塞进 if 的怀抱。
解法:map 大法好
既然 if 靠不住,那就把逻辑抽到 http 块,用 map 做判断(注意:map 不能放在 server 块内部):
http {
# 动态 HSTS 智能过滤映射
# Nginx 1.7.5+ 支持:值为空字符串时 add_header 自动隐匿
map $http_cf_ray $dynamic_hsts {
default ""; # 有 cf-ray?走 Cloudflare,HSTS 留空
"" "max-age=31536000"; # 没 cf-ray?直连源站,上 HSTS(有效期1年)
}
server {
# ...
add_header Strict-Transport-Security $dynamic_hsts always;
}
}妙处来了: Nginx 1.7.5+ 中,add_header 有个隐藏特性——值是空字符串 "" 时,这个头直接隐身,不会出现在响应里。
所以走 Cloudflare 时 $dynamic_hsts 是 "",HSTS 自动消失;直连源站时输出 max-age=31536000,完美。if 你不行,我 map 上,照样搞定。
第二坑:宝塔偷偷给我加了 X-Frame-Options
又是咋回事呢?
我想搞跨站嵌入,结果浏览器死活不让:
拒绝在 Frame 中加载,因为响应头包含了 X-Frame-Options: SAMEORIGIN我明明在反代里写了:
proxy_hide_header X-Frame-Options;Halo 后端吐的头应该被抹掉了啊,这 SAMEORIGIN 从哪冒出来的?我一度怀疑人生,是不是 Halo 在跟我作对。
排查:宝塔是内鬼
顺着配置翻啊翻,发现宝塔面板自动生成了一堆扩展 conf(注意:wuqishi.com 替换为你的实际域名):
include /www/server/panel/vhost/nginx/extension/wuqishi.com/*.conf;
include /www/server/panel/vhost/nginx/generic/wuqishi.com/*.conf;里面静静躺着:
add_header X-Frame-Options SAMEORIGIN;好家伙,宝塔你背刺我!
关键认知: proxy_hide_header 只能藏后端服务器吐的头,对 Nginx 自己配置里写的头完全没辙。宝塔全局一加,我的反代配置根本管不到,就像你在家锁了门,结果物业在小区大门又加了一道锁。
解法:不跟宝塔纠缠,直接上 CSP
去翻宝塔文件删了?太怂,而且面板一更新可能又刷回来,跟打地鼠似的。直接在反代入口写三重保险:
# 第一重:后端来的 X-Frame-Options,抹掉
proxy_hide_header X-Frame-Options;
# 第二重:宝塔全局加的?强行重置为空,浏览器看到空值直接忽略
add_header X-Frame-Options "" always;
# 第三重:终极杀招,CSP 的 frame-ancestors 优先级碾压 X-Frame-Options
# * 表示允许所有域名嵌入,如需限制改为 https://example.com
add_header Content-Security-Policy "frame-ancestors *" always;原理: 现代浏览器里,CSP 的 frame-ancestors 指令优先级远远高于老古董 X-Frame-Options。看到 frame-ancestors *,浏览器当场无视 SAMEORIGIN,跨站嵌入一路绿灯。
* 意思是 "谁都能嵌",如果你只想让特定域名嵌,如改成 frame-ancestors https://wuqishi.com 就行。但我懒,先开全通,后面再收紧。
收工,总结一下
两个坑,都是响应头的小把戏,但排查起来很费神。最后靠 map 和 CSP 组合拳搞定,没动业务逻辑,纯粹是边缘层的斗智斗勇。
nginx -t 绿灯,配置保存不报错。响应头清清爽爽,强迫症舒服了。
解决问题的乐趣就在于此,把流量在边缘层梳理得明明白白,爽。
Nginx 又双叒给我挖坑了:HSTS 撞车 + 宝塔偷偷锁 Frame
https://wuqishi.com/archives/nginx-hsts-csp-pitfall-bt-panel
评论